package com.leo.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileFilter;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @ClassName ClassUtil
 * @Description 类操作工具
 * @Author wangss
 * @date 2019.12.25 19:59
 * @Version 1.0
 */
public final class ClassUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(ClassUtil.class);

    /**
     * 获取类加载器
     *
     * @return
     */
    public static ClassLoader getClassLoader() {
        // 获取当前线程中的classloader
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * 加载类
     * jvm在装载类时会执行类的静态代码段，要记住静态代码是和class绑定的，class装载成功就表示执行了你的静态代码了，而且以后不会再执行这段静态代码了。
     * Class.forName(xxx.xx.xx)的作用是要求JVM查找并加载指定的类，也就是说JVM会执行该类的静态代码段。
     *
     * @param className     类的全名
     * @param isInitialized 是否初始化类
     * @return
     */
    public static Class<?> loadClass(String className, boolean isInitialized) {
        Class<?> cls;
        try {
            // 指定类加载器
            cls = Class.forName(className, isInitialized, getClassLoader());
        } catch (ClassNotFoundException e) {
            LOGGER.error("load class failure", e);
            throw new RuntimeException(e);
        }
        return cls;
    }

    /**
     * 加载类（默认将初始化类）
     */
    public static Class<?> loadClass(String className) {
        return loadClass(className, true);
    }

    /**
     * 获取指定包名下的所有类
     *
     * @param packageName
     * @return
     */
    public static Set<Class<?>> getClassSet(String packageName) {
        Set<Class<?>> classSet = new HashSet<>();
        try {
            // 根据包路径获取目录集（实际目录结构是以/进行区分，配置的包路径位com.leo）
            Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));

            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if (null != url) {
                    // 获取url的协议(protocol(协议)可以是 HTTP、HTTPS、FTP 和 File、jar port 为端口号，path为文件路径及文件名。)
                    String protocol = url.getProtocol();

                    // 如果是文件
                    if ("file".equals(protocol)) {
                        // 获取路径(URL中的空格有时候被编码成%20，有时候被编码成加号+)
                        // URL中关于空格的编码正是与空格所在位置相关：空格被编码成加号+的情况只会在查询字符串部分出现，而被编码成%20则可以出现在路径和查询字符串中。
                        String packagePath = url.getPath().replace("%20", "");
                        addClass(classSet, packagePath, packageName);

                    } else if ("jar".equals(protocol)) {
                        // 如果是jar包，则遍历jar包，遇见结尾为.class的文件则进行加载，并存入set集合中
                        JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
                        if (jarURLConnection != null) {
                            JarFile jarFile = jarURLConnection.getJarFile();
                            if (jarFile != null) {
                                Enumeration<JarEntry> jarEntries = jarFile.entries();
                                while (jarEntries.hasMoreElements()) {

                                    JarEntry jarEntry = jarEntries.nextElement();
                                    String jarEntryName = jarEntry.getName();
                                    if (jarEntryName.endsWith(".class")) {
                                        String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replace("/", ".");
                                        doAddClass(classSet, className);
                                    }
                                }
                            }
                        }
                    }

                }
            }
        } catch (Exception e) {
            LOGGER.error("get class set failure", e);
            throw new RuntimeException(e);
        }
        return classSet;
    }

    /**
     * 将找到的文件存放到集合中
     *
     * @param classSet
     * @param packagePath
     * @param packageName
     */
    private static void addClass(Set<Class<?>> classSet, String packagePath, String packageName) {

        // 这里相当于 File file = new File(packagePath); File[] files1 = file.listFiles();
        // listFiles()方法是返回某个目录下所有文件和目录的绝对路径，返回的是File数组
        File[] files = new File(packagePath).listFiles(new FileFilter() {
            @Override
            public boolean accept(File file) {
                // 过滤器，返回 文件目录 或者 是文件且以.class结尾的文件 组成的数组
                return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
            }
        });

        for (File file : files) {
            String fileName = file.getName();
            // 如果是文件
            if (file.isFile()) {
                // 获取文件名，并用包路径+文件名进行拼接，组成完整的路径
                String className = fileName.substring(0, fileName.lastIndexOf("."));
                if (StringUtil.isNotEmpty(packageName)) {
                    className = packageName + "." + className;
                }
                // 加载类，并存入集合中
                doAddClass(classSet, className);
            } else {
                // 如果是目录，则file名字为文件夹名称，则包路径需要重新拼接，然后递归执行addClass方法进行处理
                String subPackagePath = fileName;
                if (StringUtil.isNotEmpty(packagePath)) {
                    subPackagePath = packagePath + "/" + subPackagePath;
                }
                String subPackageName = fileName;
                if (StringUtil.isNotEmpty(packageName)) {
                    subPackageName = packageName + "." + subPackageName;
                }
                addClass(classSet, subPackagePath, subPackageName);
            }
        }
    }

    /**
     * 加载类但不初始化，并存入set集合中
     *
     * @param classSet
     * @param className
     */
    private static void doAddClass(Set<Class<?>> classSet, String className) {
        Class<?> cls = loadClass(className, false);
        classSet.add(cls);
    }
}
